package com.spzx.order.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.spzx.cart.api.RemoteCartService;
import com.spzx.cart.api.domain.CartInfo;
import com.spzx.common.core.constant.SecurityConstants;
import com.spzx.common.core.context.SecurityContextHolder;
import com.spzx.common.core.domain.R;
import com.spzx.common.core.exception.ServiceException;
import com.spzx.common.core.utils.StringUtils;
import com.spzx.common.core.utils.uuid.UUID;
import com.spzx.common.rabbit.constant.MqConst;
import com.spzx.common.rabbit.service.RabbitService;
import com.spzx.order.api.domain.OrderInfo;
import com.spzx.order.api.domain.OrderItem;
import com.spzx.order.domain.OrderLog;
import com.spzx.order.domain.vo.OrderForm;
import com.spzx.order.domain.vo.TradeVo;
import com.spzx.order.mapper.OrderInfoMapper;
import com.spzx.order.mapper.OrderItemMapper;
import com.spzx.order.mapper.OrderLogMapper;
import com.spzx.order.service.IOrderInfoService;
import com.spzx.product.api.RemoteProductService;
import com.spzx.product.api.domain.vo.SkuLockVo;
import com.spzx.product.api.domain.vo.SkuPrice;
import com.spzx.user.api.RemoteUserAddressService;
import com.spzx.user.domain.UserAddress;
import io.jsonwebtoken.lang.Collections;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;

import java.math.BigDecimal;
import java.rmi.ServerException;
import java.rmi.server.ServerCloneException;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

@Service
public class OrderInfoServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfo> implements IOrderInfoService {
    @Autowired
    private OrderInfoMapper orderInfoMapper;

    @Autowired
    private OrderItemMapper orderItemMapper;

    @Autowired
    private OrderLogMapper orderLogMapper;

    @Autowired
    private RemoteCartService remoteCartService;

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private RemoteProductService remoteProductService;

    @Autowired
    private RemoteUserAddressService remoteUserAddressService;

    @Autowired
    RabbitService rabbitService; //来自于公共模块：spzx-common-rabbit

    /**
     * 查询订单列表
     *
     * @param orderInfo 订单
     * @return 订单
     */
    @Override
    public List<OrderInfo> selectOrderInfoList(OrderInfo orderInfo) {
        return orderInfoMapper.selectOrderInfoList(orderInfo);
    }

    /**
     * 查询订单
     *
     * @param id 订单主键
     * @return 订单
     */
    @Override
    public OrderInfo selectOrderInfoById(Long id) {
        OrderInfo orderInfo = orderInfoMapper.selectById(id);
        List<OrderItem> orderItemList = orderItemMapper.selectList(new LambdaQueryWrapper<OrderItem>().eq(OrderItem::getOrderId, id));
        orderInfo.setOrderItemList(orderItemList);
        return orderInfo;
    }


    /**
     * 获取订单交易数据
     * <p>
     * 该方法用于从购物车中获取用户选中的商品信息，生成订单交易数据。
     * 主要包括：获取用户选中的购物车商品、计算总金额、生成交易号等。
     *
     * @return TradeVo 订单交易数据对象，包含订单项列表、总金额和交易号
     * @throws ServiceException 当获取购物车数据失败或购物车无选中商品时抛出异常
     */
    @Override
    public TradeVo orderTradeData() {
        // 从安全上下文中获取当前登录用户的ID，用于后续查询该用户的购物车数据
        Long userId = SecurityContextHolder.getUserId();

        // 调用远程购物车服务，获取当前用户选中的购物车商品列表
        // SecurityConstants.INNER 表示内部服务调用，不经过网关
        R<List<CartInfo>> cartInfoListResult = remoteCartService.getCartCheckedList(userId, SecurityConstants.INNER);

        // 判断远程调用是否成功，如果失败则抛出业务异常，提示具体的错误信息
        if (R.FAIL == cartInfoListResult.getCode()) {
            throw new ServiceException(cartInfoListResult.getMsg());
        }

        // 从远程调用结果中提取购物车商品数据列表
        List<CartInfo> cartInfoLIst = cartInfoListResult.getData();

        // 检查购物车是否有选中的商品，如果没有选中商品则抛出业务异常
        if (CollectionUtils.isEmpty(cartInfoLIst)) {
            throw new ServiceException("购物车无选中商品");
        }

        // 初始化订单项列表和总金额变量
        List<OrderItem> orderItemList = null;
        BigDecimal totalAmount = new BigDecimal(0);

        // 再次检查购物车商品列表是否为空（防御性编程）
        if (!CollectionUtils.isEmpty(cartInfoLIst)) {
            // 将购物车商品列表转换为订单项列表
            orderItemList = cartInfoLIst.stream().map(cartInfo -> {
                // 为每个购物车商品创建对应的订单项对象
                OrderItem orderItem = new OrderItem();
                // 使用BeanUtils复制相同属性的值（如商品ID、SKU ID、商品名称等）
                BeanUtils.copyProperties(cartInfo, orderItem);
                // 单独设置SKU价格，确保价格是最新的
                orderItem.setSkuPrice(cartInfo.getSkuPrice());
                return orderItem;
            }).collect(Collectors.toList());

            // 遍历订单项列表，计算所有商品的总金额
            for (OrderItem orderItem : orderItemList) {
                // 单个商品金额 = 单价 × 数量
                // 累加到总金额中
                totalAmount = totalAmount.add(orderItem.getSkuPrice().multiply(new BigDecimal(orderItem.getSkuNum())));
            }
        }

        // 调用生成交易号方法，为本次订单生成唯一的交易号，用于防重复提交
        String tradeNo = this.generateTradeNo(userId);

        // 创建交易数据对象，用于封装返回给前端的订单信息
        TradeVo tradeVo = new TradeVo();

        // 设置订单项列表
        tradeVo.setOrderItemList(orderItemList);

        // 设置订单总金额
        tradeVo.setTotalAmount(totalAmount);

        // 设置交易号
        tradeVo.setTradeNo(tradeNo);

        // 返回封装好的交易数据对象
        return tradeVo;
    }


    /**
     * 生成交易号（订单防重令牌）
     *
     * @param userId 用户ID，用于生成用户唯一的交易号键，确保每个用户独立生成交易号
     * @return 返回生成的32位UUID交易号字符串，用于订单提交时的防重验证
     * <p>
     * 功能说明：
     * 1. 为每个用户生成唯一的交易号，防止重复提交订单
     * 2. 将交易号存储在Redis中，设置5分钟过期时间，防止长时间占用内存
     * 3. 交易号采用UUID去除横杠的形式，保证全局唯一性
     * <p>
     * Redis存储结构：
     * Key: user:tradeNo:{userId}
     * Value: {32位UUID字符串}
     * TTL: 5分钟（防止用户长时间未操作导致的内存浪费）
     * <p>
     * 使用场景：
     * 1. 用户进入订单确认页面时生成
     * 2. 提交订单时验证交易号有效性
     * 3. 验证通过后立即删除，实现一次性令牌机制
     */
    private String generateTradeNo(Long userId) {
        // 构造Redis键名，使用用户ID确保每个用户独立生成交易号
        String userTradeKey = "user:tradeNo:" + userId;

        // 生成去横杠的UUID作为交易号，确保全局唯一性（32位字符串）
        String tradeNo = UUID.randomUUID().toString().replaceAll("-", "");

        // 将交易号存储到Redis中，设置有效期5分钟，实现临时一次性令牌
        redisTemplate.opsForValue().set(userTradeKey, tradeNo, 5, TimeUnit.MINUTES);

        return tradeNo;
    }

    /**
     * 验证页面提交流水号是否有效
     *
     * @param userId
     * @param tradeNo
     * @return
     */
    private Boolean checkTradeNo(Long userId, String tradeNo) {
        String userTradeKey = "user:tradeNo:" + userId;
        String redisTradseNo = (String) redisTemplate.opsForValue().get(userTradeKey);
        return tradeNo.equals(redisTradseNo);
    }

    /**
     * 删除用户的交易流水号
     * 通过用户ID构建Redis键值，删除对应的交易流水号记录
     *
     * @param userId 用户唯一标识符，用于构建Redis键值
     */
    private void deleteTradeNo(String userId) {
        // 构建用户交易流水号的Redis键值
        String userTradeKey = "user:tradeNo:" + userId;
        // 从Redis中删除该用户的交易流水号
        redisTemplate.delete(userTradeKey);
    }


    /**
     * 提交订单接口
     * <p>
     * 主要功能包括：
     * 1. 防止用户通过浏览器回退等方式重复提交订单（基于Redis流水号校验）；
     * 2. 校验购物项是否为空；
     * 3. 校验商品价格是否与当前系统一致，不一致则抛出异常并更新购物车价格；
     * 4. 调用下单逻辑保存订单；
     * 5. 删除已选中的购物车商品。
     *
     * @param orderForm 用户提交的订单表单数据，包含订单项、交易流水号等信息
     * @return 返回生成的订单ID
     * @throws ServiceException 当出现业务异常时抛出，如重复提交、商品未选择、价格错误、下单失败等
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public Long submitOrder(OrderForm orderForm) {
        // 从安全上下文中获取当前登录用户的ID，用于后续的权限校验和数据关联
        Long userId = SecurityContextHolder.getUserId();

        // 采用Lua脚本保证判断删除流水号原子性，防止并发情况下的重复提交问题
        // KEYS[1]: 流水号Key，ARGV[1]：用户提交的流水号
        String userTradeKey = "user:tradeNo:" + userId;

        // Lua脚本内容：原子性地检查并删除交易号
        // 如果Redis中存储的交易号与用户提交的交易号一致，则删除该交易号并返回1
        // 否则返回0，表示交易号不匹配或已过期
        String scriptText = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" + "then\n" + "    return redis.call(\"del\",KEYS[1])\n" + "else\n" + "    return 0\n" + "end";

        // 创建Redis脚本执行对象，用于执行Lua脚本
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();

        // 设置Lua脚本内容
        redisScript.setScriptText(scriptText);

        // 设置脚本执行结果类型为Long
        redisScript.setResultType(Long.class);

        // 执行Lua脚本，原子性地验证并删除交易号
        // 参数1：KEYS列表，包含userTradeKey
        // 参数2：ARGV列表，包含用户提交的交易号
        Long result = (Long) redisTemplate.execute(redisScript, Arrays.asList(userTradeKey), orderForm.getTradeNo());

        // 判断脚本执行结果，如果返回0表示交易号验证失败（可能已过期或重复提交）
        if (result == 0) {
            // 抛出业务异常，提示用户不要重复提交订单
            throw new ServiceException("请勿重复提交订单,请尝试重试");
        }

        // 2. 判断购物项是否为空，防止用户提交空订单
        List<OrderItem> orderItemList = orderForm.getOrderItemList();

        // 使用Collections工具类检查订单项列表是否为空
        if (Collections.isEmpty(orderItemList)) {
            // 如果购物项为空，抛出业务异常提示用户选择商品
            throw new ServiceException("请选择商品");
        }

        // 3.订单校验 - 价格校验
        // 3.1.校验价格，确保用户下单时商品价格与系统当前价格一致
        // 提取订单项中的所有SKU ID，用于批量查询最新价格
        List<Long> skuIdList = orderItemList.stream().map(OrderItem::getSkuId).toList();

        // 调用远程商品服务，批量获取商品的最新价格信息
        // SecurityConstants.INNER表示内部服务调用，不经过网关
        R<List<SkuPrice>> skuPriceListResult = remoteProductService.getSkuPriceList(skuIdList, SecurityConstants.INNER);

        // 检查远程调用是否成功，如果失败则抛出业务异常
        if (R.FAIL == skuPriceListResult.getCode()) {
            throw new ServiceException(skuPriceListResult.getMsg());
        }

        // 从远程调用结果中提取商品价格数据列表
        List<SkuPrice> skuPriceList = skuPriceListResult.getData();

        // 将SKU价格列表转换为Map结构，便于通过SKU ID快速查找对应价格
        // Key: SKU ID, Value: 商品销售价格
        Map<Long, BigDecimal> skuIdToSalePriceMap = skuPriceList.stream().collect(Collectors.toMap(SkuPrice::getSkuId, SkuPrice::getSalePrice));

        // 构建价格错误信息字符串缓冲区
        StringBuilder errorMsg = new StringBuilder();

        // 遍历订单项，逐一校验商品价格是否与系统最新价格一致
        for (OrderItem orderItem : orderItemList) {
            // 使用compareTo方法比较BigDecimal数值是否相等
            // 如果不相等，说明价格已变更，需要提示用户
            if (orderItem.getSkuPrice().compareTo(skuIdToSalePriceMap.get(orderItem.getSkuId())) != 0) {
                // 构建错误信息，提示用户具体哪个商品价格有误
                errorMsg.append("商品【").append(orderItem.getSkuName()).append("】价格有误，请重新选择商品！");
            }
        }

        // 检查是否存在价格错误信息
        if (StringUtils.isNotEmpty(errorMsg.toString())) {
            // 如果存在价格错误，调用远程购物车服务更新购物车中的商品价格
            remoteCartService.updateCartPrice(userId, SecurityConstants.INNER);

            // 抛出业务异常，提示用户价格有误并需要重新选择商品
            throw new ServiceException(errorMsg.toString());
        }

        // 3.2.校验库存并锁定库存（待实现）
        // 构造库存锁定参数列表，将订单项转换为库存锁定对象
        List<SkuLockVo> skuLockVoList = orderItemList.stream().map(orderItem -> {
            SkuLockVo skuLockVo = new SkuLockVo();
            skuLockVo.setSkuId(orderItem.getSkuId());
            skuLockVo.setSkuNum(orderItem.getSkuNum());
            return skuLockVo;
        }).toList();

        // 调用远程服务检查并锁定商品库存
        R<String> stringR = remoteProductService.checkAndLock(orderForm.getTradeNo(), skuLockVoList, SecurityConstants.INNER);
        if (R.FAIL == stringR.getCode()) {
            throw new ServiceException(stringR.getMsg());
        }
        String checkAndLockResult = stringR.getData();
        // 如果库存检查返回错误信息，则抛出业务异常
        if (StringUtils.isNotEmpty(checkAndLockResult)) {
            throw new ServiceException(checkAndLockResult);
        }

        // 4. 执行下单操作，保存订单信息到数据库
        Long orderId = null;
        try {
            //4 下单
            // 调用saveOrder方法保存订单信息，返回生成的订单ID
            orderId = saveOrder(orderForm);
        } catch (Exception e) {
            // 捕获保存订单过程中可能出现的异常
            e.printStackTrace();

            //4.1 下单失败，解锁库存
            rabbitService.sendMessage(MqConst.EXCHANGE_PRODUCT, MqConst.ROUTING_UNLOCK, orderForm.getTradeNo());

            // 抛出业务异常，提示用户订单创建失败
            throw new ServiceException("订单创建失败");
        }

        // 5. 删除购物车中已选中的商品，避免用户重复下单
        // 调用远程购物车服务，删除用户购物车中已选中的商品
        remoteCartService.deleteCartCheckedList(userId, SecurityConstants.INNER);

        //7.发送延迟消息,取消订单 (15分钟未支付，消费者就会进行关闭订单->解锁库存。)
        rabbitService.sendDealyMessage(MqConst.EXCHANGE_CANCEL_ORDER,
                MqConst.ROUTING_CANCEL_ORDER,
                orderId, MqConst.CANCEL_ORDER_DELAY_TIME);
        // 返回生成的订单ID，用于后续业务处理（如跳转到支付页面）
        return orderId;
    }

    /**
     * 保存订单信息
     * <p>
     * 该方法用于根据传入的订单表单数据，创建并保存一个完整的订单信息。包括订单主表、订单明细以及订单操作日志。
     * 该方法具有事务控制，遇到任何异常都会回滚。
     *
     * @param orderForm 订单表单数据，包含订单的基本信息、收货地址ID、运费、订单项列表等
     * @return 返回生成的订单ID
     * @throws ServiceException 当远程调用失败或数据异常时抛出业务异常
     */
    @Transactional(rollbackFor = Exception.class)
    public Long saveOrder(OrderForm orderForm) {
        // 从安全上下文获取当前登录用户的ID，用于关联订单与用户
        Long userId = SecurityContextHolder.getUserId();

        // 从安全上下文获取当前登录用户的名称，用于记录订单操作人
        String userName = SecurityContextHolder.getUserName();

        // 创建订单主表对象，用于存储订单的基本信息
        OrderInfo orderInfo = new OrderInfo();

        // 设置订单编号，使用前端传递的交易号作为订单号，确保唯一性
        orderInfo.setOrderNo(orderForm.getTradeNo());

        // 设置用户ID，关联订单与用户
        orderInfo.setUserId(userId);

        // 设置用户昵称，记录下单用户信息
        orderInfo.setNickName(userName);

        // 设置订单备注信息，保存用户自定义的订单备注
        orderInfo.setRemark(orderForm.getRemark());

        // 调用远程用户地址服务，根据地址ID获取用户详细地址信息
        // SecurityConstants.INNER表示内部服务调用，不经过网关
        R<UserAddress> userAddressResult = remoteUserAddressService
                .getUserAddress(orderForm.getUserAddressId(), SecurityConstants.INNER);

        // 从远程调用结果中提取用户地址数据
        UserAddress userAddress = userAddressResult.getData();

        // 设置收货人姓名，从用户地址信息中获取
        orderInfo.setReceiverName(userAddress.getName());

        // 设置收货人电话，从用户地址信息中获取
        orderInfo.setReceiverPhone(userAddress.getPhone());

        // 设置收货人标签名称（如家、公司等），从用户地址信息中获取
        orderInfo.setReceiverTagName(userAddress.getTagName());

        // 设置收货省份编码，从用户地址信息中获取
        orderInfo.setReceiverProvince(userAddress.getProvinceCode());

        // 设置收货城市编码，从用户地址信息中获取
        orderInfo.setReceiverCity(userAddress.getCityCode());

        // 设置收货区域编码，从用户地址信息中获取
        orderInfo.setReceiverDistrict(userAddress.getDistrictCode());

        // 设置详细收货地址，从用户地址信息中获取
        orderInfo.setReceiverAddress(userAddress.getFullAddress());

        // 获取订单项列表，用于计算总金额和保存订单明细
        List<OrderItem> orderItemList = orderForm.getOrderItemList();

        // 初始化订单总金额为0
        BigDecimal totalAmount = new BigDecimal(0);

        // 遍历订单项列表，计算订单总金额
        for (OrderItem orderItem : orderItemList) {
            // 单个商品金额 = 单价 × 数量
            // 累加到订单总金额中
            totalAmount = totalAmount.add(orderItem.getSkuPrice().multiply(new BigDecimal(orderItem.getSkuNum())));
        }

        // 设置订单总金额
        orderInfo.setTotalAmount(totalAmount);

        // 设置优惠券金额，初始为0
        orderInfo.setCouponAmount(new BigDecimal(0));

        // 设置订单原始总金额（未优惠前的金额）
        orderInfo.setOriginalTotalAmount(totalAmount);

        // 设置运费金额，从订单表单中获取
        orderInfo.setFeightFee(orderForm.getFeightFee());

        // 设置订单状态为0（待付款），表示新创建的订单
        orderInfo.setOrderStatus(0);

        // 将订单主表信息插入数据库
        orderInfoMapper.insert(orderInfo);

        // 保存订单明细信息，遍历订单项列表逐一保存
        for (OrderItem orderItem : orderItemList) {
            // 设置订单项关联的订单ID
            orderItem.setOrderId(orderInfo.getId());

            // 将订单项信息插入数据库
            orderItemMapper.insert(orderItem);
        }

        // 记录订单操作日志，用于跟踪订单状态变化
        OrderLog orderLog = new OrderLog();

        // 设置日志关联的订单ID
        orderLog.setOrderId(orderInfo.getId());

        // 设置处理状态为0（提交订单）
        orderLog.setProcessStatus(0);

        // 设置操作备注信息
        orderLog.setNote("提交订单");

        // 设置操作人信息
        orderLog.setOperateUser("用户");

        // 将订单日志信息插入数据库
        orderLogMapper.insert(orderLog);

        // 返回生成的订单ID，用于后续业务处理
        return orderInfo.getId();
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void processCloseOrder(Long orderId) {
        OrderInfo orderInfo = orderInfoMapper.selectById(orderId);
        if (null != orderInfo && orderInfo.getOrderStatus().intValue() == 0) { //  订单状态orderStatus=0 说明15分钟未支付。
            orderInfo.setOrderStatus(-1); //  -1 取消订单
            orderInfo.setCancelTime(new Date());
            orderInfo.setCancelReason("未支付自动取消");
            orderInfoMapper.updateById(orderInfo);

            //记录日志
            OrderLog orderLog = new OrderLog();
            orderLog.setOrderId(orderInfo.getId());
            orderLog.setProcessStatus(-1);
            orderLog.setNote("系统取消订单");
            orderLogMapper.insert(orderLog);

            //发送MQ消息通知商品系统解锁库存
            rabbitService.sendMessage(MqConst.EXCHANGE_PRODUCT,
                    MqConst.ROUTING_UNLOCK, orderInfo.getOrderNo());
        }
    }


        /**
     * 根据订单编号查询订单信息
     * @param orderNo 订单编号
     * @return 订单信息，包含订单项列表
     */
    @Override
    public OrderInfo getByOrderNo(String orderNo) {
        // 构造查询条件，根据订单编号查询订单信息
        LambdaQueryWrapper<OrderInfo> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(OrderInfo::getOrderNo, orderNo);
        OrderInfo orderInfo = orderInfoMapper.selectOne(queryWrapper);

        // 查询该订单下的所有订单项
        List<OrderItem> orderItems = orderItemMapper.selectList(
        new LambdaQueryWrapper<OrderItem>().eq(OrderItem::getOrderId, orderInfo.getId()));
        orderInfo.setOrderItemList(orderItems);
        return orderInfo;
    }

    @Override
    public void processPaySuccess(String orderNo) {

        OrderInfo orderInfo = orderInfoMapper.selectOne(new LambdaQueryWrapper<OrderInfo>()
                .eq(OrderInfo::getOrderNo, orderNo).select(OrderInfo::getId, OrderInfo::getOrderStatus));

        if (orderInfo != null && orderInfo.getOrderStatus().intValue() == 0){
            orderInfo.setOrderStatus(1);
            orderInfo.setPaymentTime(new Date());
            orderInfoMapper.updateById(orderInfo);
        }
    }
}
